package baseFx.springboot;

import baseFx.common.Assert;
import baseFx.common.Env;
import baseFx.common.context.spring.SpringApplicationContext;
import baseFx.common.utils.ClassUtils;
import baseFx.springboot.context.WebApplicationContext;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

public class FxSpringApplication extends SpringBootServletInitializer {
    protected boolean isContextWithRequest;
    private SpringApplication app;
    private WebApplicationContext applicationContext;
    private MvcEvents mvcEvents = new MvcEvents();
    private String requestContextKey = UUID.randomUUID().toString();
    static FxSpringApplication current;

    public Class<?> getMainClass() {
        return app.getMainApplicationClass();
    }

    public MvcEvents getMvcEvents() {
        return this.mvcEvents;
    }

    public void run(String... args) {
        run(getClass(), args);
    }

    public void run(Class<?> mainClass, String... args) {
        Assert.isNull(current);
        current = this;
        if (Env.getMainClass() == null) {
            Env.setMainClass(mainClass);
        }
        app = createSpringApplication(mainClass);
        app.run(args);
    }

    private void settingApplicationContext(WebApplicationContext context) {
        context.registerBean((Class<? super FxSpringApplication>) getClass(), () -> this, def -> {
            def.setPrimary(true);
        });
        context.registerBean(MvcEvents.class, () -> this.mvcEvents);
        context.registerBean(DefaultCallback.class);
        context.onRegisterBean.add((s, e) -> {
            if (e.define.isSingleton()) {
                Class<?> clazz = ClassUtils.getClassForName(e.define.getBeanClassName());
                e.isCancel = clazz != null && SpringBootServletInitializer.class.isAssignableFrom(clazz);
            }
        });
        initApplicationContext(context);
    }

    protected void initApplicationContext(WebApplicationContext context) {

    }

    protected void setWebServer(ConfigurableServletWebServerFactory factory) {

    }

    protected Integer getPort() {
        return null;
    }

    protected void appStartup(WebApplicationContext context, ApplicationArguments args) {

    }

    private void settingStartup(WebApplicationContext context, ApplicationArguments args) {
        registerRequestContext();
        appStartup(context, args);
    }

    public ApplicationContext getApplicationContext(HttpServletRequest request) {
        if (isContextWithRequest) {
            return (ApplicationContext) request.getAttribute(requestContextKey);
        } else {
            return applicationContext;
        }
    }

    protected SpringApplicationContext createRequestApplicationContext(WebApplicationContext context) {
        return context.sub();
    }

    private void registerRequestContext() {
        mvcEvents.handlerBefore.add((t, e) -> {
            if (isContextWithRequest) {
                SpringApplicationContext ctx = createRequestApplicationContext(applicationContext);
                e.handler = ctx.getBean(e.type);
                e.request.setAttribute(requestContextKey, ctx);
            }
        });
        mvcEvents.requestAfter.add((t, e) -> {
            if (isContextWithRequest) {
                Object value = e.request.getAttribute(requestContextKey);
                if (value instanceof SpringApplicationContext) {
                    ((SpringApplicationContext) value).close();
                }
            }
        });
    }

    protected SpringApplication createSpringApplication(Class<?> mainClass) {
        if (app == null) {
            app = new SpringApplication(mainClass) {
                @Override
                protected ConfigurableApplicationContext createApplicationContext() {
                    if (applicationContext == null) {
                        applicationContext = new WebApplicationContext();
                        settingApplicationContext(applicationContext);
                    }
                    return applicationContext;
                }
            };
        }
        return app;
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        builder.contextClass(WebApplicationContext.class);
        builder.initializers(context -> {
            applicationContext = (WebApplicationContext) context;
            settingApplicationContext(applicationContext);
        });
        return builder.sources(getClass());
    }

    private class DefaultCallback implements ApplicationRunner, WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
        @Override
        public void run(ApplicationArguments args) {
            settingStartup(applicationContext, args);
        }

        @Override
        public void customize(ConfigurableServletWebServerFactory factory) {
            if (getPort() != null) {
                factory.setPort(getPort());
            }
            setWebServer(factory);
        }
    }
}
